package jsr292.java.lang.invoke;

import java.util.Collections;

import jsr292.weaver.opt.OptimizerContext;
import jsr292.weaver.opt.OptimizerVarAllocator;

public abstract class MethodHandle {
    final MethodType methodType;

    protected MethodHandle(MethodType methodType) {
        this.methodType=methodType;
    }

    public final MethodType type() {
        return methodType;
    }  

    @Override
    public final String toString() {
        return "MethodHandle " + getClass().getName();
    }
    
    public Object $invoke$() throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object o1) throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object o1, Object o2) throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object o1, Object o2, Object o3) throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object o1, Object o2, Object o3, Object o4) throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object o1, Object o2, Object o3, Object o4, Object o5) throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object o1, Object o2, Object o3, Object o4, Object o5, Object o6) throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object o1, Object o2, Object o3, Object o4, Object o5, Object o6, Object o7) throws Throwable {
        throw fail();
    }
    @SuppressWarnings("unused") public Object $invoke$(Object... rest) throws Throwable {
        throw fail();
    }
    
    
    // kernel API support
    
    public final Object invokeWithArguments(Object[] args) throws Throwable {
        if (args == null) {  // better to avoid to go through the switch in that case
            return $invoke$();
        }
        switch(args.length) {
        case 0:
            return $invoke$();
        case 1:
            return $invoke$(args[0]);
        case 2:
            return $invoke$(args[0], args[1]);
        case 3:
            return $invoke$(args[0], args[1], args[2]);
        case 4:
            return $invoke$(args[0], args[1], args[2], args[3]);
        case 5:
            return $invoke$(args[0], args[1], args[2], args[3], args[4]);
        case 6:
            return $invoke$(args[0], args[1], args[2], args[3], args[4], args[5]);
        case 7:
            return $invoke$(args[0], args[1], args[2], args[3], args[4], args[5], args[6]);
        default:
            return $invoke$(args);    
        }
    }
    
    public final Object invokeWithArguments(java.util.List<?> args) throws Throwable {
        if (args == null) {  // better to avoid to go through the switch in that case
            return $invoke$();
        }
        switch(args.size()) {
        case 0:
            return $invoke$();
        case 1:
            return $invoke$(args.get(0));
        case 2:
            return $invoke$(args.get(0), args.get(1));
        case 3:
            return $invoke$(args.get(0), args.get(1), args.get(2));
        case 4:
            return $invoke$(args.get(0), args.get(1), args.get(2), args.get(3));
        case 5:
            return $invoke$(args.get(0), args.get(1), args.get(2), args.get(3), args.get(4));
        case 6:
            return $invoke$(args.get(0), args.get(1), args.get(2), args.get(3), args.get(4), args.get(5));
        case 7:
            return $invoke$(args.get(0), args.get(1), args.get(2), args.get(3), args.get(4), args.get(5), args.get(6));
        default:
            return $invoke$(args.toArray());    
        }
    }
    
    // this method is overriden by MhVarargsCollector.asType()
    public MethodHandle asType(MethodType newType) {
        MethodType type = type();
        if (type.parameterCount() != newType.parameterCount())
            throw new IllegalArgumentException("cannot convert to "+newType+": "+type);
        
        
        if (type== newType || !Conversions.needConversions(type, newType, type.parameterCount()))
            return new MHConvertDelegator(this, newType);
        
        
        return new MHConverter(this, newType, type.erase(), newType.erase());
    }
    
    public final MethodHandle asCollector(Class<?> arrayType, int arrayLength) {
        if (!arrayType.isArray()) {
            throw new IllegalArgumentException("not an arrayType "+arrayType);
        }
        int length = methodType.parameterCount();
        if (length == 0 ||
                !methodType.parameterType(length - 1).isAssignableFrom(arrayType)) {
            throw new IllegalArgumentException("the array type is not assignable to last parameter");
        }
        
        return asCollector0(arrayType.getComponentType(), arrayLength);
    }
    
    final MethodHandle asCollector0(Class<?> componentType, int arrayLength) {
        if (arrayLength < 0) {
            throw new IllegalArgumentException("negative array length");
        }
        
        if (componentType.isPrimitive())  { //FIXME
            throw new UnsupportedOperationException("array of primitive types are not yet supported");
        }
        
        MethodType type = type();
        int typeLength = type.parameterCount();
        MethodType newType = type.dropParameterTypes(typeLength - 1, typeLength);
        newType = newType.insertParameterTypes(typeLength - 1, Collections.<Class<?>>nCopies(arrayLength, componentType));
        return new MHCollector(this, newType, arrayLength, componentType);
    }
    
    public final MethodHandle asSpreader(Class<?> arrayType, int arrayLength) {
        if (!arrayType.isArray()) {
            throw new IllegalArgumentException("not an arrayType "+arrayType);
        }
        int length = methodType.parameterCount();
        if (arrayLength>length) {
            throw new IllegalArgumentException("not enough parameters");
        }
        
        if (arrayType.getComponentType().isPrimitive()) { //FIXME
            throw new UnsupportedOperationException("array of primitive types are not yet supported");
        }
        
        int newLength = 1 + length - arrayLength;
        Class<?>[] parameterTypes = new Class<?>[newLength];
        System.arraycopy(methodType.parameterTypes, 0, parameterTypes, 0, newLength - 1);
        parameterTypes[newLength - 1] = arrayType;
        
        return new MHSpreader(this, arrayLength, MethodType.makeInternal(methodType.returnType, true, parameterTypes));
    }
    
    public MethodHandle asVarargsCollector(Class<?> arrayType) {
        if (!arrayType.isArray()) {
            throw new IllegalArgumentException("not an arrayType "+arrayType);
        }
        if (arrayType.getComponentType().isPrimitive()) { //FIXME
            throw new UnsupportedOperationException("array of primitive types are not yet supported");
        }
        
        return asVarargsCollector0(arrayType);
    }
    
    MethodHandle asVarargsCollector0(Class<?> arrayType) {
        int length = methodType.parameterCount();
        if (length == 0 ||
            !methodType.parameterType(length - 1).isAssignableFrom(arrayType)) {
            throw new IllegalArgumentException("the array type is not assignable to last parameter");
        }
        
        return new MHVarargsCollector(methodType, arrayType.getComponentType(), this);
    }
    
    public final boolean isVarargsCollector() {
        return this instanceof MHVarargsCollector;
    }
    
    public final MethodHandle bindTo(Object value) {
        return MethodHandles.insertArguments(this, 0, value);
    }
    
    
    // Optimizer support
    
    public abstract boolean isWeavable(Class<?> classBeingRedefined);
    public abstract void weave(OptimizerContext context, OptimizerVarAllocator allocator);
    
    
    protected static final UnsupportedOperationException fail() {
        return new UnsupportedOperationException();    
    }
    protected static final AssertionError assertion() {
        return new AssertionError();    
    }
}
